package org.gwt.pluginframework.common;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import org.gwt.pluginframework.common.event.CommonEvents;
import org.gwt.pluginframework.common.event.IPluginEvent;
import org.gwt.pluginframework.common.event.IPluginEventHandler;
import org.gwt.pluginframework.common.event.PluginEvent;
import org.gwt.pluginframework.common.exception.NoSuchPluginException;
import org.gwt.pluginframework.common.exception.PluginException;
import org.gwt.pluginframework.common.exception.RegistrationException;

import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.impl.SchedulerImpl;

/**
 * Plug-in manager implementation.
 * 
 * FIXME
 * maybe we should treat pluginManager as plug-in to eliminate special events (with no invoker)
 * it would require to implement IPlugin interface by PluginManager
 * 
 * @author mszymanski
 */
public class PluginManager implements IPluginManager {
	private static final long serialVersionUID = 5257674817758136162L;

	public static final String ALL_EVENTS = "ALL";
	
	/**
	 * Main container to store plug-ins and enable easy access by Id.
	 */
	protected Map<String, IPlugin> register = new HashMap<String, IPlugin>();

	/**
	 * For event name there is a list of plug-ins which registered themselves
	 * in manager to get types (event names) of events.
	 * Broadcast optimization.
	 */
	protected Map<String, List<IPluginWithIPluginEventHandler>> registeredHandlers = 
			new HashMap<String, List<IPluginWithIPluginEventHandler>>();

	/**
	 * Configuration properties.
	 */
	protected Map<String, String> properties = new HashMap<String, String>();

	/**
	 * List of plugins which are to be started.
	 */
	protected Set<IPlugin> startup = new HashSet<IPlugin>();

	protected Scheduler scheduler = new SchedulerImpl();
	
	public void run() {
		// initialize();
		// dispatch(new Event(CommonEvents.PLUGIN_MANAGER_INITIALIZED_EVENT));
		initializePlugins();
	}

	/**
	 * All registered plug-ins at setup will be initialized.
	 */
	protected void initializePlugins() {
		// FIXME - build start map based on dependency
		final List<IPlugin> list = new ArrayList<IPlugin>(register.values());
		Collections.sort(list);
		scheduler.scheduleIncremental(new Scheduler.RepeatingCommand() {
			Iterator<IPlugin> i = list.iterator();

			@Override
			public boolean execute() {
				if (!i.hasNext()) {
					// dispatch(new
					// Event(CommonEvents.PLUGIN_MANAGER_LOADED_EVENT));
					startPlugins();
					return false;
				} else {
					IPlugin plugin = i.next();
					try {
						plugin.initialize();
					} catch (Exception e) {
						GWT.log("Exception occured during star!", e);
						handle(new PluginException(plugin, e));
					}
					return true;
				}
			}
		});
	}

	/**
	 * Only selected at setup are started.
	 */
	public void startPlugins() {
		// FIXME - build start map based on dependency
		final List<IPlugin> list = new ArrayList<IPlugin>(startup);
		Collections.sort(list);
		scheduler.scheduleIncremental(new Scheduler.RepeatingCommand() {
			Iterator<IPlugin> i = list.iterator();

			@Override
			public boolean execute() {
				if (!i.hasNext()) {
					dispatch(new PluginEvent(
							CommonEvents.PLUGIN_MANAGER_STARTED_EVENT));
					return false;
				} else {
					IPlugin plugin = i.next();
					try {
						plugin.start();
					} catch (Exception e) {
						GWT.log("Exception occured during star!", e);
						handle(new PluginException(plugin, e));
					}
					return true;
				}
			}
		});
	}

	public void setConfiguration(Map<String, String> props) {
		this.properties.putAll(props);
	}

	protected void dispatch(final String eventName, final IPluginEvent event) {
		final Iterator<IPluginWithIPluginEventHandler> i = 
				(registeredHandlers.get(eventName) != null) ? registeredHandlers.get(eventName).iterator() : null;
		
		scheduler.scheduleIncremental(new Scheduler.RepeatingCommand() {
			@Override
			public boolean execute() {
				if (i != null && i.hasNext()) {
					IPluginWithIPluginEventHandler container = i.next();
					try {
						// FIXME only started plugins? and what about the ones
						// to be started by event
						// should this be done by universal evet handled by
						// PluginManager - startPlugin {name=""} ?
						// if(plugin.isStarted()) {
						// }
						container.handler.handle(event);
					} catch (Exception e) {
						if (e instanceof PluginException) {
							handle((PluginException) e);
						} else {
							handle(new PluginException(container.plugin, e));
						}
					}
					return true;
				} else {
					return false;
				}
			}
		});
	}
	
	public void dispatch(final IPluginEvent event) {
		// dispatch to registered listeners
		final String name = event.getName();
		if (registeredHandlers.containsKey(name)) {
			dispatch(name, event);
		}

		// special plug-ins which want every single event 
		// example: EventConsol which records all communication
		dispatch(ALL_EVENTS, event);
		
		// plug-in manager can handle some of events
		handle(event);
	}
	
	/**
	 * Helper for handle event method.
	 * 
	 * @param event
	 * @param name
	 * @return
	 */
	public static boolean eventCheck(IPluginEvent event, String name) {
		return name.equals(event.getName());
	}
	
	protected void handle(final IPluginEvent event) {
		if(eventCheck(event, CommonEvents.PLUGIN_DESTROYED_EVENT)) {
			scheduler.scheduleDeferred(new Scheduler.ScheduledCommand() {
				@Override
				public void execute() {
					unregisterPlugin(event.getInvoker());
					event.getInvoker().setPluginManager(null);
				}
			});
		}
	}

	/**
	 * Methods handles exceptions from plug-ins. Dispatches exception event which
	 * can be handled by specific plug-ins (plugged exception handlers).
	 * 
	 * @param exception
	 */
	public void handle(Exception exception) {
		IPluginEvent event = null;

		if(exception instanceof PluginException) {
			event = new PluginEvent(CommonEvents.EXCEPTION_EVENT, ((PluginException) exception).getPlugin(),
					new String[] {"exception"}, new Object[] {exception});
		} else {
			event = new PluginEvent(CommonEvents.EXCEPTION_EVENT,
					new String[] {"exception"}, new Object[] {exception});
		}
		
		dispatch(event);
	}

	public void registerPlugin(IPlugin plugin) throws RegistrationException {
		registerPlugin(plugin, true);
	}

	public IPlugin getPlugin(String name) throws NoSuchPluginException {
		if (!register.containsKey(name)) {
			throw new NoSuchPluginException();
		}
		return register.get(name);
	}

	public Collection<IPlugin> getPlugins() {
		return register.values();
	}

	public void registerPlugin(IPlugin plugin, boolean start)
			throws RegistrationException {
		if (plugin.isManaged()) {
			throw new IllegalStateException("Plugin is already managed!");
		}
		register.put(plugin.getManifest().getName(), plugin);
		plugin.setPluginManager(this);
		if (start) {
			startup.add(plugin);
		}
	}

	public void unregisterPlugin(IPlugin plugin) {
		// clear dispatching lists
		for(Entry<String, List<IPluginWithIPluginEventHandler>> entry : registeredHandlers.entrySet()) {
			for(IPluginWithIPluginEventHandler container : entry.getValue()) {
				if(plugin.equals(container.plugin)) {
					entry.getValue().remove(container.handler);
				}
			}
		}
		
		// remove from startup and register
		startup.remove(plugin);
		register.remove(plugin.getName());
	}
	
	public void registerEventHandler(IPlugin plugin, IPluginEventHandler handler) {
		String eventName = handler.getEventName();
		List<IPluginWithIPluginEventHandler> list = registeredHandlers.get(eventName);
		if(list == null) {
			list = new ArrayList<IPluginWithIPluginEventHandler>();
			registeredHandlers.put(eventName, list);
		}
		list.add(new IPluginWithIPluginEventHandler(plugin, handler));
	}
	
	public void registerEventHandler(IPlugin plugin, IPluginEventHandler[] handlers) {
		for(IPluginEventHandler handler : handlers) {
			registerEventHandler(plugin, handler);
		}
	}
}

class IPluginWithIPluginEventHandler {
    public IPlugin plugin;
    public IPluginEventHandler handler;

    IPluginWithIPluginEventHandler(IPlugin plugin, IPluginEventHandler handler) {
        this.plugin = plugin;
        this.handler = handler;
    }
}